/*
 * Copyright 2009 Mike Cumings
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kenai.jbosh;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;

/**
 * Implementation of the {@code AbstractBody} class which allows for the
 * definition of messages from individual elements of a body.
 * <p/>
 * A message is constructed by creating a builder, manipulating the
 * configuration of the builder, and then building it into a class instance, as
 * in the following example:
 * 
 * <pre>
 * ComposableBody body = ComposableBody.builder()
 * 		.setNamespaceDefinition(&quot;foo&quot;, &quot;http://foo.com/bar&quot;)
 * 		.setPayloadXML(&quot;&lt;foo:data&gt;Data to send to remote server&lt;/foo:data&gt;&quot;)
 * 		.build();
 * </pre>
 * 
 * Class instances can also be "rebuilt", allowing them to be used as templates
 * when building many similar messages:
 * 
 * <pre>
 * ComposableBody body2 = body.rebuild()
 * 		.setPayloadXML(&quot;&lt;foo:data&gt;More data to send&lt;/foo:data&gt;&quot;).build();
 * </pre>
 * 
 * This class does only minimal syntactic and semantic checking with respect to
 * what the generated XML will look like. It is up to the developer to protect
 * against the definition of malformed XML messages when building instances of
 * this class.
 * <p/>
 * Instances of this class are immutable and thread-safe.
 */
public final class ComposableBody extends AbstractBody {

	/**
	 * Pattern used to identify the beginning {@code body} element of a BOSH
	 * message.
	 */
	private static final Pattern BOSH_START = Pattern.compile("<"
			+ "(?:(?:[^:\t\n\r >]+:)|(?:\\{[^\\}>]*?}))?" + "body"
			+ "(?:[\t\n\r ][^>]*?)?" + "(/>|>)");

	/**
	 * Map of all attributes to their values.
	 */
	private final Map<BodyQName, String> attrs;

	/**
	 * Payload XML.
	 */
	private final String payload;

	/**
	 * Computed raw XML.
	 */
	private final AtomicReference<String> computed = new AtomicReference<String>();

	/**
	 * Class instance builder, after the builder pattern. This allows each
	 * message instance to be immutable while providing flexibility when
	 * building new messages.
	 * <p/>
	 * Instances of this class are <b>not</b> thread-safe.
	 */
	public static final class Builder {
		private Map<BodyQName, String> map;
		private boolean doMapCopy;
		private String payloadXML;

		/**
		 * Prevent direct construction.
		 */
		private Builder() {
			// Empty
		}

		/**
		 * Creates a builder which is initialized to the values of the provided
		 * {@code ComposableBody} instance. This allows an existing
		 * {@code ComposableBody} to be used as a template/starting point.
		 * 
		 * @param source
		 *            body template
		 * @return builder instance
		 */
		private static Builder fromBody(final ComposableBody source) {
			Builder result = new Builder();
			result.map = source.getAttributes();
			result.doMapCopy = true;
			result.payloadXML = source.payload;
			return result;
		}

		/**
		 * Set the body message's wrapped payload content. Any previous content
		 * will be replaced.
		 * 
		 * @param xml
		 *            payload XML content
		 * @return builder instance
		 */
		public Builder setPayloadXML(final String xml) {
			if (xml == null) {
				throw (new IllegalArgumentException(
						"payload XML argument cannot be null"));
			}
			payloadXML = xml;
			return this;
		}

		/**
		 * Set an attribute on the message body / wrapper element.
		 * 
		 * @param name
		 *            qualified name of the attribute
		 * @param value
		 *            value of the attribute
		 * @return builder instance
		 */
		public Builder setAttribute(final BodyQName name, final String value) {
			if (map == null) {
				map = new HashMap<BodyQName, String>();
			} else if (doMapCopy) {
				map = new HashMap<BodyQName, String>(map);
				doMapCopy = false;
			}
			if (value == null) {
				map.remove(name);
			} else {
				map.put(name, value);
			}
			return this;
		}

		/**
		 * Convenience method to set a namespace definition. This would result
		 * in a namespace prefix definition similar to:
		 * {@code <body xmlns:prefix="uri"/>}
		 * 
		 * @param prefix
		 *            prefix to define
		 * @param uri
		 *            namespace URI to associate with the prefix
		 * @return builder instance
		 */
		public Builder setNamespaceDefinition(final String prefix,
				final String uri) {
			BodyQName qname = BodyQName.createWithPrefix(
					XMLConstants.XML_NS_URI, prefix,
					XMLConstants.XMLNS_ATTRIBUTE);
			return setAttribute(qname, uri);
		}

		/**
		 * Build the immutable object instance with the current configuration.
		 * 
		 * @return composable body instance
		 */
		public ComposableBody build() {
			if (map == null) {
				map = new HashMap<BodyQName, String>();
			}
			if (payloadXML == null) {
				payloadXML = "";
			}
			return new ComposableBody(map, payloadXML);
		}
	}

	// /////////////////////////////////////////////////////////////////////////
	// Constructors:

	/**
	 * Prevent direct construction. This constructor is for body messages which
	 * are dynamically assembled.
	 */
	private ComposableBody(final Map<BodyQName, String> attrMap,
			final String payloadXML) {
		super();
		attrs = attrMap;
		payload = payloadXML;
	}

	/**
	 * Parse a static body instance into a composable instance. This is an
	 * expensive operation and should not be used lightly.
	 * <p/>
	 * The current implementation does not obtain the payload XML by means of a
	 * proper XML parser. It uses some string pattern searching to find the
	 * first @{code body} element and the last element's closing tag. It is
	 * assumed that the static body's XML is well formed, etc.. This
	 * implementation may change in the future.
	 * 
	 * @param body
	 *            static body instance to convert
	 * @return composable bosy instance
	 * @throws BOSHException
	 */
	static ComposableBody fromStaticBody(final StaticBody body)
			throws BOSHException {
		String raw = body.toXML();
		Matcher matcher = BOSH_START.matcher(raw);
		if (!matcher.find()) {
			throw (new BOSHException(
					"Could not locate 'body' element in XML.  The raw XML did"
							+ " not match the pattern: " + BOSH_START));
		}
		String payload;
		if (">".equals(matcher.group(1))) {
			int first = matcher.end();
			int last = raw.lastIndexOf("</");
			if (last < first) {
				last = first;
			}
			payload = raw.substring(first, last);
		} else {
			payload = "";
		}

		return new ComposableBody(body.getAttributes(), payload);
	}

	/**
	 * Create a builder instance to build new instances of this class.
	 * 
	 * @return AbstractBody instance
	 */
	public static Builder builder() {
		return new Builder();
	}

	/**
	 * If this {@code ComposableBody} instance is a dynamic instance, uses this
	 * {@code ComposableBody} instance as a starting point, create a builder
	 * which can be used to create another {@code ComposableBody} instance based
	 * on this one. This allows a {@code ComposableBody} instance to be used as
	 * a template. Note that the use of the returned builder in no way modifies
	 * or manipulates the current {@code ComposableBody} instance.
	 * 
	 * @return builder instance which can be used to build similar
	 *         {@code ComposableBody} instances
	 */
	public Builder rebuild() {
		return Builder.fromBody(this);
	}

	// /////////////////////////////////////////////////////////////////////////
	// Accessors:

	/**
	 * {@inheritDoc}
	 */
	public Map<BodyQName, String> getAttributes() {
		return Collections.unmodifiableMap(attrs);
	}

	/**
	 * {@inheritDoc}
	 */
	public String toXML() {
		String comp = computed.get();
		if (comp == null) {
			comp = computeXML();
			computed.set(comp);
		}
		return comp;
	}

	/**
	 * Get the paylaod XML in String form.
	 * 
	 * @return payload XML
	 */
	public String getPayloadXML() {
		return payload;
	}

	// /////////////////////////////////////////////////////////////////////////
	// Private methods:

	/**
	 * Escape the value of an attribute to ensure we maintain valid XML syntax.
	 * 
	 * @param value
	 *            value to escape
	 * @return escaped value
	 */
	private String escape(final String value) {
		return value.replace("'", "&apos;");
	}

	/**
	 * Generate a String representation of the message body.
	 * 
	 * @return XML string representation of the body
	 */
	private String computeXML() {
		BodyQName bodyName = getBodyQName();
		StringBuilder builder = new StringBuilder();
		builder.append("<");
		builder.append(bodyName.getLocalPart());
		for (Map.Entry<BodyQName, String> entry : attrs.entrySet()) {
			builder.append(" ");
			BodyQName name = entry.getKey();
			String prefix = name.getPrefix();
			if (prefix != null && prefix.length() > 0) {
				builder.append(prefix);
				builder.append(":");
			}
			builder.append(name.getLocalPart());
			builder.append("='");
			builder.append(escape(entry.getValue()));
			builder.append("'");
		}
		builder.append(" ");
		builder.append(XMLConstants.XMLNS_ATTRIBUTE);
		builder.append("='");
		builder.append(bodyName.getNamespaceURI());
		builder.append("'>");
		if (payload != null) {
			builder.append(payload);
		}
		builder.append("</body>");
		return builder.toString();
	}

}
